import ddf.minim.*;
import ddf.minim.signals.*;
import ddf.minim.analysis.*;
import ddf.minim.effects.*;

import java.awt.Color;

import org.apache.commons.lang.mutable.*;
import org.apache.commons.lang.math.*;
import org.apache.commons.lang.exception.*;
import org.apache.commons.lang.reflect.*;
import org.apache.commons.lang.*;
import org.apache.commons.lang.enums.*;
import org.apache.commons.lang.time.*;
import org.apache.commons.lang.builder.*;

import com.google.common.net.*;
import com.google.common.io.*;
import com.google.common.collect.*;
import com.google.common.base.*;
import com.google.common.primitives.*;
import com.google.common.annotations.*;
import com.google.common.util.concurrent.*;
import com.google.common.base.internal.*;

import ddf.minim.AudioPlayer;
import ddf.minim.Minim;

import guicomponents.*;
import processing.serial.*;

// default frame rate is 60/second

// if true, the device will always be active, regardless of RFID or sonar
// data
// for debugging
private static final boolean ALWAYS_ACTIVE = false;

private static boolean USE_DEACTIVATE_FOREVER = true;
private static boolean DEACTIVATE_FOREVER = false;

// show the heatmap on the GUI for debugging
private static final boolean SHOW_HEATMAP = false;

// data that will be sent for bar graph
private static final int[] MONTH_GRAPH_DATA = { 
  666, 545, 343, 560, 786, 850, 830, 501, 303, 403, 555, 602
};

// milliseconds the rings will remain on without any interaction from the
// user
private static final long RINGS_TIME = 45 * 1000;

// milliseconds the RFID display will remain on without any interaction from
// the user
private static final long RFID_TIME = 5000;

// I sure hope this stays in sync between this file and Controller.pde!
// values that define if we are below, at, or above the goal
private static final int LOWER_GOAL_THRESHOLD = 400; // watts
private static final int UPPER_GOAL_THRESHOLD = 600; // watts

// the count of pixels on the LED matrix
private static final int LOCATION_SIZE = 16;

private static final int BAUD_RATE = 9600;

// you may have to change this for your system
private static final boolean isWindows = false;

private static final String SLAVE_INPUT_SERIAL_PORT = isWindows ? "COM3" : "/dev/tty.usbserial-A600eI3u";
private static final String SLAVE_OUTPUT_SERIAL_PORT = isWindows ? "COM7" : "/dev/tty.usbserial-A600eH2l";

// used for sound
Minim minim;
GLabel labelOut;
GLabel labelWattage;
long timer;

PImage backgroundImage;

// talks to the input slave
Serial serialInput;

// talks to the output slave
Serial serialOutput;

final Model model = new Model();

/**
 	 * A list that supports getting averages over the last [m ... m + n] values
 	 */
class AverageList {
  private final List<Integer> list = new LinkedList<Integer>();

  /**
   		 * Prepend val to the list
   		 * 
   		 * @param val
   		 */
  public void prepend(final int val) {
    list.add(0, val);
  }

  /**
   		 * Gets the average value from index `start` to index `start +
   		 * averageSamples` Returns null if `start + averageSamples` is out of
   		 * bounds
   		 * 
   		 * @param start
   		 * @param averageSamples
   		 * @return
   		 */
  public Integer getAverageFrom(int start, final int averageSamples) {
    if (list.size() < start + averageSamples) {
      return null;
    }

    int sum = 0;
    for (int i = 0; i < averageSamples; i++) {
      sum += list.get(start + i);
    }

    int average = sum / averageSamples;
    return average;
  }
}

Heatmap heatmap = new Heatmap();
private static final int MAX_WEIGHT = 340;

class Heatmap {
  public byte[][] getColors() {
    int[][] weights = getWeights();
    byte[][] result = new byte[weights.length][weights.length];

    for (int i = 0; i < weights.length; i++) {
      for (int j = 0; j < weights[0].length; j++) {

        int rgb = Color.HSBtoRGB(1 + (weights[i][j] / 360f), SATURATION / 255f, BRIGHTNESS / 255f);

        int r = (rgb >> 16) & 0xFF;
        int g = (rgb >> 8) & 0xFF;
        int b = rgb & 0xFF;

        r = (int) map(r, 0, 255, 0, 7);
        g = (int) map(g, 0, 255, 0, 7);
        b = (int) map(b, 0, 255, 0, 3);

        // Bits 5-7 are red, 2-4 are green, and 0-1 are blue.
        byte rgbArduino = (byte) ((r << 5) | (g << 2) | b);
        result[i][j] = rgbArduino;
      }
    }

    return result;
  }

  private void addWeight(int[][] weights, int dataRow, int dataCol, int dataVal) {
    for (int weightRow = 0; weightRow < weights.length; weightRow++) {
      for (int weightCol = 0; weightCol < weights[0].length; weightCol++) {
        float distance = dist(weightRow, weightCol, dataRow, dataCol);
        weights[weightRow][weightCol] += (1 / (distance + 1)) * dataVal;
      }
    }
  }

  // public for debugging
  public int[][] getWeights() {
    int[][] weights = new int[LOCATION_SIZE][LOCATION_SIZE];
    for (int row = 0; row < LOCATION_SIZE; row++) {
      for (int col = 0; col < LOCATION_SIZE; col++) {
        if (model.getUsageForLocation(col, row) > 0) {
          addWeight(weights, row, col, model.getUsageForLocation(col, row));
        }
      }
    }

    for (int i = 0; i < weights.length; i++) {
      for (int j = 0; j < weights[0].length; j++) {
        weights[i][j] = (int) map(weights[i][j], 0, getMaxWattage(), 0, MAX_WEIGHT);
      }
    }

    return weights;
  }

  private int getMaxWattage() {
    int maxWattage = 0;
    for (Appliance appliance : appliances) {
      maxWattage = Math.max(maxWattage, appliance.wattage);
    }
    return maxWattage;
  }
}

/**
 	 * Maintains data about energy usage
 	 */
class Model {
  private int[][] locationUsage = new int[LOCATION_SIZE][LOCATION_SIZE];
  AverageList cumulativeUsage = new AverageList();

  // the count of results returned by recentAverages()
  private static final int RECENT_AVERAGES_COUNT = 12;

  // the number of samples used for each recent average
  private static final int SAMPLES_PER_RECENT_AVERAGE = 170;

  // how many samples will be used to determine the current average and
  // slope
  private final int AVERAGE_SAMPLES = 60;

  // this may cause us to run out of memory eventually
  // if that becomes an issue, use `list = list.sublist(0,
  // TRUNCATE_VALUE);`
  public void record(final int usage) {
    cumulativeUsage.prepend(usage);
  }

  public void clearLocationData() {
    locationUsage = new int[LOCATION_SIZE][LOCATION_SIZE];
  }

  public int getUsageForLocation(int x, int y) {
    return locationUsage[x][y];
  }

  /**
   		 * Get the slope of the current enery usage.
   		 * 
   		 * @return either DECREASING, INCREASING, or NO_CHANGE
   		 */
  public String getSlope() {
    Integer newestAverage = cumulativeUsage.getAverageFrom(0, AVERAGE_SAMPLES);
    Integer olderAverage = cumulativeUsage.getAverageFrom(AVERAGE_SAMPLES, AVERAGE_SAMPLES);

    if (newestAverage == null || olderAverage == null) {
      return null;
    }

    if (newestAverage.equals(olderAverage)) {
      return NO_CHANGE;
    }

    return newestAverage < olderAverage ? DECREASING : INCREASING;
  }

  /**
   		 * Get the recent average value.
   		 * 
   		 * @return either BELOW_GOAL, AT_GOAL, or ABOVE_GOAL
   		 */
  public String getAverage() {
    Integer average = cumulativeUsage.getAverageFrom(0, AVERAGE_SAMPLES);
    if (average == null) {
      return null;
    }

    if (average < LOWER_GOAL_THRESHOLD) {
      return BELOW_GOAL;
    }

    return average < UPPER_GOAL_THRESHOLD ? AT_GOAL : ABOVE_GOAL;
  }

  public void record(int usage, int x, int y) {
    int yCoord = (int) map(y, 0, NORMAL_SCREEN_HEIGHT, 0, LOCATION_SIZE);
    int xCoord = (int) map(x, 0, NORMAL_SCREEN_WIDTH, 0, LOCATION_SIZE);
    locationUsage[yCoord][xCoord] += usage;
  }

  public int[] getRecentAverages() {
    List<Integer> averages = new LinkedList<Integer>();

    for (int averageIndex = 0; averageIndex < RECENT_AVERAGES_COUNT; averageIndex++) {
      Integer average = cumulativeUsage.getAverageFrom(averageIndex * SAMPLES_PER_RECENT_AVERAGE, 
      SAMPLES_PER_RECENT_AVERAGE);

      if (average == null) {
        break;
      }
      averages.add(average);
    }

    int[] result = new int[averages.size()];
    for (int i = 0; i < averages.size(); i++) {
      result[i] = averages.get(i);
    }

    return result;
  }
}

class Timer {
  private long start;
  private long length;

  public Timer(long length) {
    this.length = length;
    reset();
  }

  public boolean expired() {
    return start + length < millis();
  }

  public void reset() {
    start = millis();
  }
}

Input input = new Input();

// I wish Processing supported enums
private final String NORTH = "north";
private final String EAST = "east";
private final String SOUTH = "south";
private final String WEST = "west";

class Input {
  private final String RFID = "RFID";
  private final String SONAR = "SONAR";

  private Integer recentSonarVal = null;
  private String recentRFIDVal = null;

  void parseData(String message) {
    if (message.indexOf(MESSAGE_END) == message.length() - 1) {
      message = message.substring(0, message.length() - 1);
    }
    String[] args = StringUtils.split(message, ARG_DELIMITER);
    if (args[0].contains(COMMAND_REQUEST)) {
      // if the slave is requesting a command, this gets handled
      // separately in serialEvent()
      return;
    }

    if (args.length < 2) {
      println("Unrecognized command from input (not enough args): " + args[0]);
      return;
    }

    if (args[0].equals(RFID)) {
      recentRFIDVal = args[1];
      if (recentRFIDVal.equals("null")) {
        recentRFIDVal = null;
      }

      return;
    } 
    else if (args[0].equals(SONAR)) {
      try {
        recentSonarVal = Integer.parseInt(args[1]);
      } 
      catch (NumberFormatException e) {
        // sometimes the input may be garbled
        // so we'll just ignore it and wait for a better reading
        println("Invalid sonar data from input: " + args[1]);
      }
    } 
    else {
      println("Unrecognized command from input: " + args[0]);
    }
  }

  Integer getSonar() {
    return recentSonarVal;
  }

  String getRFID() {
    return recentRFIDVal;
  }
}

public void serialEvent(Serial port) {
  final String message = port.readString();
  if (port.equals(serialInput)) {
    input.parseData(message);
  }

  if (message.startsWith(COMMAND_REQUEST)) {
    output.sendBufferedCommmand(port);
  }

  final String portName = port.equals(serialInput) ? "input" : "output";
  System.out.println(String.format("%s: %s", portName, message));
}

private final String ARG_DELIMITER = ",";
private final char MESSAGE_END = ';';

private final String INCREASING = "increasing";
private final String DECREASING = "decreasing";
private final String NO_CHANGE = "no change";

private final String BELOW_GOAL = "below goal";
private final String AT_GOAL = "at goal";
private final String ABOVE_GOAL = "above goal";

Output output = new Output();

private final String COMMAND_REQUEST = "command complete";

class Action<F> implements Function<F, Void> {

  public Void apply(F arg0) {
    doWork(arg0);
    return null;
  }

  public void doWork(F arg0) {
  }
}

class HeatmapSender extends Action<Serial> {
  private byte[] data;

  public HeatmapSender(byte[] data) {
    if (data == null) {
      throw new IllegalArgumentException("Data can't be null.");
    }

    this.data = data;
  }

  public void doWork(Serial serial) {
  }

  public boolean equals(Object obj) {
    return obj instanceof HeatmapSender && Arrays.equals(((HeatmapSender) obj).data, data);
  }
}

class MessageSender extends Action<Serial> {
  private String message;

  public MessageSender(String message) {
    if (message == null) {
      throw new IllegalArgumentException("Message can't be null.");
    }

    this.message = message;
  }

  public void doWork(Serial serial) {
    println(message + MESSAGE_END);
    serial.write(message + MESSAGE_END);
  }
  
  public boolean sameMessageAs(MessageSender other) {
    if (message.indexOf(ARG_DELIMITER) == -1) {
      return message.equals(other.message);
    }
    String command = message.substring(0, message.indexOf(ARG_DELIMITER));
    return other.message.startsWith(command);
  }

  public boolean equals(Object obj) {
    return obj instanceof MessageSender && ((MessageSender) obj).message.equals(message);
  }
}

private final String HEATMAP = "heatmap";

class Output {
  private final String ACTIVATE_PETALS_AND_FLOWER = "activate petals and flower";
  private final String DEACTIVATE_PETALS_AND_FLOWER = "deactivate petals and flower";

  private final String TIME_GRAPH = "time graph";
  private final String BAR_GRAPH = "bar graph";
  private final String RINGS = "rings";

  private LinkedList<Action<Serial>> commands = new LinkedList<Action<Serial>>();

  void sendBufferedCommmand(Serial serial) {
    if (!commands.isEmpty()) {
      commands.poll().doWork(serial);
      println(commands.size() + " items in queue.");
    }
  }

  void activatePetalsAndFlower() {
    bufferMessage(ACTIVATE_PETALS_AND_FLOWER);
  }

  void deactivatePetalsAndFlower() {
    bufferMessage(DEACTIVATE_PETALS_AND_FLOWER);
  }

  void timeGraph(int[] graphData) {
    graph(TIME_GRAPH, graphData);
  }

  void barGraph(int[] graphData) {
    graph(BAR_GRAPH, graphData);
  }

  private void graph(String graphType, int[] graphData) {
    bufferMessage(joinArgs(graphType, graphData));
  }

  void rings(String change, String goal) {
    if (!(change == INCREASING || change == DECREASING || change == NO_CHANGE)) {
      throw new IllegalArgumentException(String.format("Change must be %s or %s, but was: %s", INCREASING, 
      DECREASING, change));
    }
    if (!(goal == BELOW_GOAL || goal == AT_GOAL || goal == ABOVE_GOAL)) {
      throw new IllegalArgumentException(String.format("Change must be %s, %s, or %s, but was: %s", 
      BELOW_GOAL, AT_GOAL, ABOVE_GOAL));
    }

    bufferMessage(join(RINGS, change, goal));
  }

  void bufferMessage(final String message) {
    if (message == null) {
      throw new IllegalArgumentException("Cannot send a null message to serialOutput.");
    }

    MessageSender messageSender = new MessageSender(message);
    if (commands.isEmpty() || !commands.getLast().equals(messageSender)) {
      // if the last command is the same as this one, overwrite it
      if (! commands.isEmpty() && commands.getLast() instanceof MessageSender && messageSender.sameMessageAs((MessageSender)commands.getLast())) {
        commands.removeLast();
      }
      commands.add(messageSender);
      println("Adding to queue: " + message);
      println(commands.size() + " items in message queue");
    }
  }

  private String join(Object... objs) {
    return StringUtils.join(objs, ARG_DELIMITER);
  }

  private String joinArgs(String command, int[] args) {
    List<String> strs = new LinkedList<String>();
    for (Integer i : args) {
      strs.add(i.toString());
    }
    return join(command, StringUtils.join(strs, ARG_DELIMITER));
  }

  private int heatmapMessagesUnsent;
  public void heatmap() {
    byte[][] heatmapData = heatmap.getColors();

    if (heatmapMessagesUnsent >= heatmapData.length - 1) {
      return;
    }

    for (final byte[] heatmapRow : heatmapData) {
      commands.add(new Action<Serial>() {
        public void doWork(Serial serial) {
          serial.write(HEATMAP + ",");
          serial.write(heatmapRow);
          serial.write(MESSAGE_END);

          println(String.format("%s,[data]%s", HEATMAP, MESSAGE_END));
          
          heatmapMessagesUnsent--;
        }
      });  
    heatmapMessagesUnsent++;
    }
  }
}

class Appliance {
  // imageName excludes file extension

  // the image when the appliance is on
  public String onImageName;

  // the image when the appliance is off
  public String offImageName;
  private final String imageExt = ".png";

  public String label;
  public int x, y;
  public boolean on;
  public int wattage;

  public GImageButton button;
  private PApplet parent;

  private AudioPlayer onNoise;
  private String noiseName;

  public Appliance(PApplet parent, String onImageName, String offImageName, String label, int x, int y, 
  int wattage) {
    this.onImageName = onImageName;
    this.offImageName = offImageName;
    this.label = label;
    this.x = x;
    this.y = y;
    this.wattage = wattage;
    this.parent = parent;
  }

  public Appliance(PApplet parent, String onImageName, String offImageName, String label, int x, int y, 
  int wattage, String songName) {
    this(parent, onImageName, offImageName, label, x, y, wattage);
    noiseName = songName;
  }

  // this method will crash if called before setup()
  // thus, it can't happen in the constructor.
  public void init() {
    makeButton();
    if (noiseName != null) {
      onNoise = minim.loadFile(noiseName);
    }
  }

  /**
   		 * Hides the current button and makes a new one based on whether or not
   		 * the appliance is currently on. If you call this enough times, the app
   		 * will crash, because G4P caches, and I'm not sure how to clear it.
   		 * 
   		 * If this becomes an issue, we could just make two buttons and set them
   		 * to be visible and invisible.
   		 */
  private void makeButton() {
    if (button != null) {
      button.setVisible(false);
    }
    button = new GImageButton(parent, null, (on ? onImageName : offImageName) + imageExt, 1, x, y);
  }

  public void onClick() {
    on = !on;

    if (noiseName != null) {
      if (on) {
        onNoise.loop();
      } 
      else {
        onNoise.pause();
      }
    }

    int totalWattage = 0;
    for (Appliance appliance : appliances) {
      if (appliance.on) {
        totalWattage += appliance.wattage;
      }
    }

    labelOut.setText(String.format("%s turned %s", label, on ? "on" : "off"));
    labelWattage.setText(String.format("Total wattage: %dW", totalWattage));

    makeButton();
  }
}

Appliance[] appliances = { 
  new Appliance(this, "tv", "tv-off", "Television", 101, 495, 250, "tv.mp3"), 
  new Appliance(this, "wm", "wm-off", "Washing Machine", 240, 97, 500, "wm.mp3"), 
  new Appliance(this, "cfl", "cfl-off", "Compact Flourescent Bulb", 745, 300, 10), 
  new Appliance(this, "lightbulb", "lightbulb-off", "Incandescent Bulb", 480, 80, 40), 
  new Appliance(this, "fridge", "fridge-off", "Refrigerator", 730, 534, 540, "refrigerator.mp3"), 
  new Appliance(this, "fan", "fan-off", "Fan", 75, 65, 10, "fan.mp3"), 
  new Appliance(this, "vacuum", "vacuum-off", "Vacuum", 400, 320, 300, "vacuum.mp3"), 
  new Appliance(this, "microwave", "microwave-off", "Microwave", 645, 130, 1000, "microwave.mp3")
};

private static final int NORMAL_SCREEN_WIDTH = 911;
private static final int NORMAL_SCREEN_HEIGHT = 715;
private static final int SCREEN_WIDTH = SHOW_HEATMAP ? 920 : NORMAL_SCREEN_WIDTH;
private static final int SCREEN_HEIGHT = SHOW_HEATMAP ? 1164 : NORMAL_SCREEN_HEIGHT;

public void setup() {
  // this must be the same size as the background image
  size(SCREEN_WIDTH, SCREEN_HEIGHT);

  colorMode(HSB);
  imageMode(CENTER);

  minim = new Minim(this);

  if (!Arrays.asList(Serial.list()).contains(SLAVE_INPUT_SERIAL_PORT)) {
    println("Warning: slave input is not a valid serial port: " + SLAVE_INPUT_SERIAL_PORT);
    println("Valid serial ports are: ");
    println(Serial.list());
  }

  if (!Arrays.asList(Serial.list()).contains(SLAVE_OUTPUT_SERIAL_PORT)) {
    println("Warning: slave output is not a valid serial port: " + SLAVE_OUTPUT_SERIAL_PORT);
    println("Valid serial ports are: ");
    println(Serial.list());
  }

  serialInput = new Serial(this, SLAVE_INPUT_SERIAL_PORT, BAUD_RATE);
  serialInput.bufferUntil(MESSAGE_END);
  serialOutput = new Serial(this, SLAVE_OUTPUT_SERIAL_PORT, BAUD_RATE);
  serialOutput.bufferUntil(MESSAGE_END);

  // Load the background image
  backgroundImage = loadImage(SHOW_HEATMAP ? "Floorplan-heatmap.png" : "Floorplan.png");
  G4P.setMouseOverEnabled(true);
  cursor(CROSS);

  for (Appliance appliance : appliances) {
    appliance.init();
  }

  labelOut = new GLabel(this, "", 0, 665, 920, 40);
  labelOut.setTextAlign(GAlign.CENTER);
  labelOut.setFont("Arial", 24);
  labelWattage = new GLabel(this, "", 0, 687, 920, 40);
  labelWattage.setTextAlign(GAlign.CENTER);
  labelWattage.setFont("Arial", 14);
  timer = millis() - 5000;
  
  output.deactivatePetalsAndFlower();
}

// true if the device is currently activated
private boolean active = false;

private Timer ringsTimer = new Timer(RINGS_TIME);
private Timer rfidTimer = new Timer(RFID_TIME);
private boolean rfidTimerExpiredLastDraw = false;

String currentlyShowingRFID;

public void draw() {
  background(backgroundImage);

  if (SHOW_HEATMAP) {
    drawHeatmap();
  }

  if (millis() - timer > 4000) {
    labelOut.setText("Click an appliance to turn it on or off");
  }
  
  if (DEACTIVATE_FOREVER && USE_DEACTIVATE_FOREVER) {
    return;
  }

  boolean sonarFound = checkSonar();
  String rfidID = input.getRFID();
  boolean rfidFound = rfidID != null;

  if (!active && !ALWAYS_ACTIVE) {
    if (sonarFound || rfidFound) {
      active = true;
      output.activatePetalsAndFlower();
    } 
    else {
      return;
    }
  }

  // if we see sonar motion, reset the sonar timer
  if (sonarFound) {
    println("sonar rings timer reset");
    ringsTimer.reset();
  }

  // if we get an RFID reading, reset the RFID timer
  if (rfidFound) {
    // println("RFID reset: " + rfidID);
    currentlyShowingRFID = rfidID;
    rfidTimer.reset();
    rfidTimerExpiredLastDraw = false;
  }

  updateModel();

  if (rfidTimer.expired()) {
    currentlyShowingRFID = null;
    if (!rfidTimerExpiredLastDraw) {
      rfidTimerExpiredLastDraw = true;
      ringsTimer.reset();
    }
  } 
  else {
    sendRFIDCommands(currentlyShowingRFID);
    return;
  }

  if (ringsTimer.expired() && !ALWAYS_ACTIVE) {
    active = false;
    println("Rings timer expired - deactivating");
    output.deactivatePetalsAndFlower();
    return;
  }

  sendRingsCommands();
}

private static final int SATURATION = 150;
private static final int BRIGHTNESS = 150;
private static final int X_OFFSET = 150;
private static final int Y_OFFSET = 300;
private static final int ELLIPSE_SIZE = 20;
private static final int ELLIPSE_SPACING = 30;

private void drawHeatmap() {
  ellipseMode(CENTER);

  int[][] weights = heatmap.getWeights();
  for (int row = 0; row < LOCATION_SIZE; row++) {
    for (int col = 0; col < LOCATION_SIZE; col++) {
      fill(MAX_WEIGHT - weights[col][row], SATURATION, BRIGHTNESS);
      ellipse(X_OFFSET + col * ELLIPSE_SPACING, Y_OFFSET + row * ELLIPSE_SPACING, ELLIPSE_SIZE, ELLIPSE_SIZE);
    }
  }
}

private void sendRFIDCommands(String rfidID) {
  if (rfidID == null)
    return;
  if (rfidID.equals(NORTH)) {
    output.heatmap();
    return;
  }

  if (rfidID.equals(SOUTH)) {
    // not sure what to do here

    // for demo
    println("deactivate from RFID");
    output.deactivatePetalsAndFlower();
    DEACTIVATE_FOREVER = true;
    return;
  }

  if (rfidID.equals(WEST)) {
    output.barGraph(MONTH_GRAPH_DATA);
    return;
  }

  if (rfidID.equals(EAST)) {
    output.timeGraph(model.getRecentAverages());
  }
}

private void sendRingsCommands() {
  String slope = model.getSlope();
  String average = model.getAverage();
  if (slope != null && average != null) {
    output.rings(slope, average);
  }
}

private AverageList sonarVals = new AverageList();

// the count of sonar samples used to determine if there is motion
private final static int SONAR_SAMPLES = 4;

// the difference two readings must show to be considered significant
private static final Integer SONAR_THRESHOLD = 30;

private boolean checkSonar() {
  Integer sonarVal = input.getSonar();
  if (sonarVal != null) {
    sonarVals.prepend(sonarVal);
  }

  Integer newestAverage = sonarVals.getAverageFrom(0, SONAR_SAMPLES);
  Integer olderAverage = sonarVals.getAverageFrom(SONAR_SAMPLES, SONAR_SAMPLES);

  if (newestAverage == null || olderAverage == null) {
    return false;
  }
  
  return newestAverage < olderAverage - SONAR_THRESHOLD || newestAverage - SONAR_THRESHOLD > olderAverage;
}

private void updateModel() {
  // if we want the heatmap to only be for a single tick, we must do this
  model.clearLocationData();

  int totalEnergy = 0;
  for (Appliance appliance : appliances) {
    if (appliance.on) {
      totalEnergy += appliance.wattage;
      model.record(appliance.wattage, appliance.x, appliance.y);
    }
  }

  model.record(totalEnergy);
}

void handleImageButtonEvents(GImageButton imageButton) {
  for (Appliance appliance : appliances) {
    if (appliance.button.equals(imageButton)) {
      appliance.onClick();
    }
  }

  timer = millis();
}

public void stop() {
  minim.stop();
  super.stop();
}

